Skip to content

perf(auth): cache account pool store scans#163

Merged
amondnet merged 6 commits into
mainfrom
amondnet/perf-cache-claude-account-pool-store-scan-avoid
Jul 15, 2026
Merged

perf(auth): cache account pool store scans#163
amondnet merged 6 commits into
mainfrom
amondnet/perf-cache-claude-account-pool-store-scan-avoid

Conversation

@amondnet

@amondnet amondnet commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Cache account-pool store scans by the store directory's modification time. Previously, every pooled request traversed resolve_pool_accountsscan_accountsread_account_uuid, performing one directory read plus one credential-file read and JSON parse per account. The shared process-wide auth::shared::scan_cached helper now serves the previous scan when the directory mtime is unchanged, reducing steady-state discovery to one metadata stat and zero credential-file reads.

The existing metadata lookup from #118 supplies the cache signal, so this adds no extra stat. Directory changes still invalidate the cache for no-restart account discovery, scans remain on spawn_blocking, absent stores retain the cheap NotFound short-circuit, and platforms without an mtime bypass the cache. Claude and Codex use separate store paths and can both benefit, while the primary improvement is avoiding Claude UUID reads.

Closes #151
Part of #149 (Tier 1).

Milestone / spec

Performance follow-up tracked by #149 (Tier 1) and #151. This internal refactor does not change observable behavior, configuration, endpoints, CLI behavior, or provider/model semantics, so no README.md, docs/, or site/ update is needed.

Checklist

  • cargo build passes
  • cargo test passes (new behavior is covered; tests run without network/loopback where possible)
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --all --check clean
  • Source files stay under 500 lines
  • English only; matches surrounding style
  • Frozen spec in docs/ updated if this change deviates from it
  • User-facing docs updated for behavior/config/endpoint/CLI/provider/model changes — README.md / site/ as applicable (wiki/ is generated; don't hand-edit)
  • Any new GitHub Action is pinned to a full commit SHA

Notes for reviewers

  • scan_cached_serves_cache_until_mtime_changes_and_bypasses_without_mtime injects mtimes directly to verify cache hits, invalidation, and no-mtime bypass behavior.
  • resolve_pool_accounts_caches_scan_across_unchanged_requests verifies against the real filesystem that an unchanged second request does not re-scan.
  • Validation completed with cargo fmt --all --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features --workspace (529 library tests plus all integration tests, including multi_account).
  • Review the mtime invalidation boundary and cache synchronization in auth::shared closely.

Summary by cubic

Caches pooled-account store scans by store-directory mtime to skip repeat credential-file reads during discovery; biggest win is avoiding Claude UUID reads. The cache is keyed by provider and directory; unchanged stores now cost one stat and zero credential-file reads, with behavior unchanged.

  • Performance
    • Added a process-wide scan cache keyed by (provider, dir) and invalidated by the store’s mtime.
    • Reuses the existing metadata check; preserves the NotFound short-circuit and runs on spawn_blocking.
    • Best-effort invalidation: coarse mtimes may defer a re-scan; platforms without mtime bypass the cache.
    • Strengthened tests: refreshed results replace cached ones; verify provider isolation and same-provider hits; confirm pooled-request reuse.

Written for commit 299f489. Summary will update on new commits.

Avoid repeated credential-file reads while the account store directory remains unchanged, while preserving mtime-based no-restart discovery.

Refs #151
@amondnet amondnet added the type:refactor Code refactoring without behavior change label Jul 15, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an in-memory caching mechanism for account directory scans in src/auth/shared.rs using the directory's modification time (mtime) to optimize steady-state request performance by avoiding redundant file I/O. It includes the implementation of scan_cached, a process-wide cache, and corresponding unit tests. The review feedback suggests refactoring scan_cached to be asynchronous and wrapping the synchronous directory scanning in tokio::task::spawn_blocking to prevent blocking the async runtime worker threads.

Comment thread src/auth/shared.rs Outdated
@codspeed-hq

codspeed-hq Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 11 untouched benchmarks


Comparing amondnet/perf-cache-claude-account-pool-store-scan-avoid (299f489) with main (32bbeb0)

Open in CodSpeed

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

amondnet added 4 commits July 15, 2026 19:28
Review of the mtime-keyed account-pool scan cache surfaced a latent
cross-provider hazard and several inaccurate comments:

- Key the shared cache by (provider, dir), not dir alone, so two stores
  pointed at the same directory (SHUNT_CLAUDE_ACCOUNTS_DIR ==
  SHUNT_CODEX_ACCOUNTS_DIR) can never serve one provider's scan (e.g.
  Codex's UUID-less entries) to the other.
- Scope the zero-read claim to account-list discovery (a full request
  still reads the selected account file), reword the stale per-request
  scan line, and frame directory mtime as a best-effort invalidation
  signal (coarse-resolution filesystems can defer a re-scan one tick).
- Soften the concurrent-miss comment: an entry is only served while its
  stored mtime matches, so a stale write is re-scanned away next request.
- Strengthen the invalidation test to prove the refreshed account set
  replaces the cached one (not just that it re-scans) and to assert a
  second provider keeps its own entry at the same path.

Refs #151
…tion test

Second review pass on the cache comments:

- Note that provider_label now also partitions the scan cache, not just
  the error text.
- Correct the coarse-mtime caveat: a change sharing the cached scan's
  timestamp goes unnoticed until a later change advances the mtime (not
  merely "within one clock tick").
- State the concurrent-miss guarantee precisely: snapshots may differ and
  the last write wins, but an entry is served only while its stored mtime
  equals the mtime the request sampled, so a stale write is re-scanned
  away once a request observes a different mtime.
- Strengthen the test with a second same-provider call proving the entry
  is retained (cache hit), not just isolated from the other provider.

Refs #151
@amondnet
amondnet marked this pull request as ready for review July 15, 2026 11:16

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a process-wide mtime-keyed cache for account-pool store scans in auth::shared, so steady-state account-list discovery costs one directory stat and zero credential-file reads instead of a full read_dir plus one JSON parse per account on every pooled request.

  • scan_cache / scan_cached: A Mutex<HashMap<(provider, dir), CachedScan>> is initialized once via OnceLock; cache hits return the cloned account list, misses scan without holding the lock, and insert after. Concurrent misses race to insert (last write wins), with mtime-based invalidation ensuring eventual correctness; platforms without mtime bypass the cache entirely.
  • resolve_pool_accounts: The single fs::metadata call now also extracts the mtime and feeds it to scan_cached, preserving the NotFound short-circuit and the blocking-pool invariant while adding zero extra stats.
  • Tests: scan_cached_serves_cache_until_mtime_changes_and_bypasses_without_mtime uses injected mtimes to verify first-miss, same-mtime hit, invalidation with a refreshed result, provider isolation, and no-mtime bypass; resolve_pool_accounts_caches_scan_across_unchanged_requests confirms end-to-end reuse against the real filesystem.

Confidence Score: 5/5

Safe to merge; the change is a pure performance optimization with no observable behavior change.

The caching logic is correct: mtime is sampled before the scan, the lock is dropped during I/O to avoid blocking concurrent hits, and the no-mtime bypass ensures correctness on every platform. Concurrency trade-offs are well-documented in code comments.

No files require special attention.

Important Files Changed

Filename Overview
src/auth/shared.rs Adds a process-wide mtime-keyed scan cache (scan_cache / scan_cached) and wires it into resolve_pool_accounts; logic is sound, concurrency trade-offs are documented and correct, tests cover first-miss, cache-hit, invalidation-with-refreshed-output, provider isolation, and no-mtime bypass.
.claude/agent-memory/review-review-comment-analyzer/shunt-account-scan-cache-comment-rot.md New agent-memory document recording comment-rot patterns for the mtime scan cache; content is accurate and directly applicable to future reviews of this module.
.claude/agent-memory/review-review-pr-test-analyzer/shunt-account-scan-cache-coverage.md New agent-memory document cataloguing test-coverage gaps for PR #163; the injected-mtime test now does assert the refreshed result after invalidation (lines 720-724), so the "never asserts changed scan output" note is slightly stale relative to the shipped test.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant R as Pooled Request
    participant B as spawn_blocking
    participant FS as Filesystem
    participant C as scan_cache (Mutex)

    R->>B: resolve_pool_accounts(provider, dir, scan)
    B->>FS: fs::metadata(dir)
    alt dir absent (NotFound)
        FS-->>B: Err(NotFound)
        B-->>R: Ok([])
    else stat succeeds
        FS-->>B: Ok(meta) → mtime
        B->>C: lock → get((provider, dir))
        alt "cache hit (stored mtime == current mtime)"
            C-->>B: accounts.clone()
            B-->>R: Ok(cached accounts)
        else cache miss
            C-->>B: None or stale mtime
            Note over B: drop lock before I/O
            B->>FS: scan() → read_dir + per-file reads
            FS-->>B: accounts
            B->>C: "lock → insert((provider, dir), {mtime, accounts})"
            B-->>R: Ok(fresh accounts)
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant R as Pooled Request
    participant B as spawn_blocking
    participant FS as Filesystem
    participant C as scan_cache (Mutex)

    R->>B: resolve_pool_accounts(provider, dir, scan)
    B->>FS: fs::metadata(dir)
    alt dir absent (NotFound)
        FS-->>B: Err(NotFound)
        B-->>R: Ok([])
    else stat succeeds
        FS-->>B: Ok(meta) → mtime
        B->>C: lock → get((provider, dir))
        alt "cache hit (stored mtime == current mtime)"
            C-->>B: accounts.clone()
            B-->>R: Ok(cached accounts)
        else cache miss
            C-->>B: None or stale mtime
            Note over B: drop lock before I/O
            B->>FS: scan() → read_dir + per-file reads
            FS-->>B: accounts
            B->>C: "lock → insert((provider, dir), {mtime, accounts})"
            B-->>R: Ok(fresh accounts)
        end
    end
Loading

Reviews (2): Last reviewed commit: "docs(auth): reflow account scan cache co..." | Re-trigger Greptile

Comment thread src/auth/shared.rs Outdated
@sonarqubecloud

Copy link
Copy Markdown

@amondnet
amondnet merged commit b42b1d4 into main Jul 15, 2026
16 checks passed
@amondnet
amondnet deleted the amondnet/perf-cache-claude-account-pool-store-scan-avoid branch July 15, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:refactor Code refactoring without behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: cache Claude account-pool store scan (avoid per-request N+1 file I/O)

1 participant